<?php

namespace Swof\Foundation;

use Exception;
use Swof\Config\Repository;
use Swof\Container\Container;
use Swof\Event\Event;
use Swof\Foundation\Bootstrap\RegisterFacades;
use Swof\Foundation\Support\Providers\RouteServiceProvider;
use Swof\Http\HttpRequest;
use Swof\Http\WebSocketRequest;
use Swof\Routing\Router;
use Swof\Routing\RoutingServiceProvider;
use Swof\Server\Http\HttpServer;
use Swof\Server\Socket\WebSocketServer;
use Swof\Support\ServiceProvider;

class Application extends Container
{
    protected const SWOF_WELCOME = "
      _____                     _____     ___
     /  __/             ____   /  __/  __/  /__   ___ __    __  __
     \__ \  | | /| / / / __ \  \__ \  /_   ___/  /  _`  |  |  \/ /
     __/ /  | |/ |/ / / /_/ /  __/ /   /  /_    |  (_|  |  |   _/
    /___/   |__/\__/  \____/  /___/    \___/     \___/\_|  |__|
    ";

    /**
     * 框架基础目录
     *
     * @var string
     */
    protected $basePath;

    /**
     * All of the registered service providers.
     *
     * @var ServiceProvider[]
     */
    protected $serviceProviders = [];

    /**
     * The names of the loaded service providers.
     *
     * @var array
     */
    protected $loadedProviders = [];

    /**
     * The array of booted callbacks.
     *
     * @var callable[]
     */
    protected $bootedCallbacks = [];

    /**
     * @throws Exception
     */
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->basePath = $basePath;
        }

        $this->registerBaseBindings();
        $this->registerBaseServiceProviders();
        $this->registerCoreContainerAliases();
        (new RegisterFacades())->bootstrap($this);

        $this->initRoutes();

        echo self::SWOF_WELCOME . PHP_EOL;
    }

    /**
     * Register the basic bindings into the container.
     *
     * @return void
     * @throws Exception
     */
    protected function registerBaseBindings()
    {
        static::setInstance($this);

        $this->instance('app', $this);

        $this->instance(Container::class, $this);

        $this->instance('config', new Repository());
    }

    /**
     * Register all of the base service providers.
     *
     * @return void
     * @throws Exception
     */
    protected function registerBaseServiceProviders()
    {
        $this->register(new RoutingServiceProvider($this));
        $this->register(new RouteServiceProvider($this));
    }

    /**
     * Note: 注册核心类
     *
     * @throws Exception
     * @author: Zhao Zhiqiang
     * @since: 2021/5/22 11:23
     */
    public function registerCoreContainerAliases()
    {
        /*
         * Request 类每次请求时都不同，因此放到进程周期
         */
        foreach ([
                    'request' => new HttpRequest(),
                    'wsRequest' => new WebSocketRequest()
                 ] as $key => $alias) {

            $this->singleton($key, $alias);
        }
    }

    /**
     * Register a service provider with the application.
     *
     * @param ServiceProvider|string $provider
     * @param bool $force
     * @return ServiceProvider|string|null
     * @throws Exception
     */
    public function register(ServiceProvider $provider, bool $force = false)
    {
        if (($registered = $this->getProvider($provider)) && !$force) {
            return $registered;
        }

        $provider->register();

        // If there are bindings / singletons set as properties on the provider we
        // will spin through them and register them with the application, which
        // serves as a convenience layer while registering a lot of bindings.
        if (property_exists($provider, 'bindings')) {
            foreach ($provider->bindings as $key => $value) {
                $this->bind($key, $value);
            }
        }

        if (property_exists($provider, 'singletons')) {
            foreach ($provider->singletons as $key => $value) {
                $this->singleton($key, $value);
            }
        }

        $this->markAsRegistered($provider);

        // If the application has already booted, we will call this boot method on
        // the provider class so it has an opportunity to do its boot logic and
        // will be ready for any usage by this developer's application logic.
        $this->bootProvider($provider);

        return $provider;
    }

    /**
     * Get the registered service provider instance if it exists.
     *
     * @param ServiceProvider|string $provider
     * @return ServiceProvider|null
     */
    public function getProvider(ServiceProvider $provider): ?ServiceProvider
    {
        return array_values($this->getProviders($provider))[0] ?? null;
    }

    /**
     * Get the registered service provider instances if any exist.
     *
     * @param ServiceProvider $provider
     * @return array
     */
    public function getProviders(ServiceProvider $provider): array
    {
        $name = get_class($provider);

        return array_filter($this->serviceProviders, function ($value) use ($name) {
            return $value instanceof $name;
        });
    }

    /**
     * Mark the given provider as registered.
     *
     * @param ServiceProvider $provider
     * @return void
     */
    protected function markAsRegistered(ServiceProvider $provider)
    {
        $this->serviceProviders[] = $provider;

        $this->loadedProviders[get_class($provider)] = true;
    }

    /**
     * Boot the given service provider.
     *
     * @param ServiceProvider $provider
     * @return void
     */
    protected function bootProvider(ServiceProvider $provider)
    {
        $provider->callBootingCallbacks();

        if (method_exists($provider, 'boot')) {
            $this->call([$provider, 'boot']);
        }

        $provider->callBootedCallbacks();
    }

    /**
     * @throws Exception
     */
    public function initRoutes()
    {
        $this->singleton('route', Router::instance()->registerRoute());
        $this->singleton('event', $this->registerEvent());
    }

    /**
     * Note: register listener into event
     *
     * @return Event
     * @author: Zhao Zhiqiang
     * @since: 2021/6/2 17:48
     */
    public function registerEvent(): Event
    {
        $event = new Event();

        $files = scandir($this->basePath().'/app/Listeners');

        foreach ($files as $file) {
            if ($file === '.' || $file === '..') {
                continue;
            }

            $class = 'App\\Listeners\\'.\explode('.', $file)[0];

            if (class_exists($class)) {
                $listener = new $class;
                $event->register($listener->getName(), [$listener, 'handle']);
            }
        }

        return $event;
    }

    /**
     * 安装目录
     *
     * @param string $path
     * @return string
     * @author: Zhao Zhiqiang
     * @since: 2021/5/12 9:46
     */
    public function basePath(string $path = ''): string
    {
        return $this->basePath . ($path ? DIRECTORY_SEPARATOR . $path : $path);
    }

    /**
     * Note: 运行
     *
     * @author: Zhao Zhiqiang
     * @since: 2021/5/16 13:34
     */
    public function run($argv)
    {
        switch ($argv[1]) {
            case 'http:start':
                $server = new HttpServer($this);
                break;
            case 'ws:start':
                $server = new WebSocketServer($this);
                break;
            default:
                $server = null;
                break;
        }

        $server->start();
    }

    public function call($callback)
    {

    }

    /**
     * Determine if the application routes are cached.
     *
     * @return bool
     */
    public function routesAreCached(): bool
    {
        return file_exists($this->getCachedRoutesPath());
    }

    /**
     * Get the path to the routes cache file.
     *
     * @return string
     */
    public function getCachedRoutesPath(): string
    {
        return $this->normalizeCachePath('APP_ROUTES_CACHE', 'cache/routes-v7.php');
    }

    /**
     * Normalize a relative or absolute path to a cache file.
     *
     * @param string $key
     * @param string $default
     * @return string
     */
    protected function normalizeCachePath(string $key, string $default): string
    {
        return $this->bootstrapPath($default);
    }

    /**
     * Get the path to the bootstrap directory.
     *
     * @param string $path Optionally, a path to append to the bootstrap path
     * @return string
     */
    public function bootstrapPath(string $path = ''): string
    {
        return $this->basePath.DIRECTORY_SEPARATOR.'bootstrap'.($path ? DIRECTORY_SEPARATOR.$path : $path);
    }

    /**
     * Register a new "booted" listener.
     *
     * @param  callable  $callback
     * @return void
     */
    public function booted($callback)
    {
        $this->bootedCallbacks[] = $callback;

        $this->fireAppCallbacks([$callback]);
    }

    /**
     * Call the booting callbacks for the application.
     *
     * @param  callable[]  $callbacks
     * @return void
     */
    protected function fireAppCallbacks(array $callbacks)
    {
        foreach ($callbacks as $callback) {
            $callback($this);
        }
    }
}